import graphene
from graphene import relay

from ...core.permissions import PagePermissions
from ...menu import models
from ...product.models import Collection
from ..channel.dataloaders import ChannelBySlugLoader
from ..channel.types import ChannelContext, ChannelContextType
from ..core.connection import CountableDjangoObjectType
from ..page.dataloaders import PageByIdLoader
from ..product.dataloaders import (
    CategoryByIdLoader,
    CollectionByIdLoader,
    CollectionChannelListingByCollectionIdAndChannelSlugLoader,
)
from ..translations.fields import TranslationField
from ..translations.types import MenuItemTranslation
from ..utils import get_user_or_app_from_context
from .dataloaders import (
    MenuByIdLoader,
    MenuItemByIdLoader,
    MenuItemChildrenLoader,
    MenuItemsByParentMenuLoader,
)


class Menu(ChannelContextType, CountableDjangoObjectType):
    items = graphene.List(lambda: MenuItem)

    class Meta:
        default_resolver = ChannelContextType.resolver_with_context
        description = (
            "Represents a single menu - an object that is used to help navigate "
            "through the store."
        )
        interfaces = [relay.Node]
        only_fields = ["id", "name", "slug"]
        model = models.Menu

    @staticmethod
    def resolve_items(root: ChannelContext[models.Menu], info, **_kwargs):
        menu_items = MenuItemsByParentMenuLoader(info.context).load(root.node.id)
        return menu_items.then(
            lambda menu_items: [
                ChannelContext(node=menu_item, channel_slug=root.channel_slug)
                for menu_item in menu_items
            ]
        )


class MenuItem(ChannelContextType, CountableDjangoObjectType):
    children = graphene.List(lambda: MenuItem)
    url = graphene.String(description="URL to the menu item.")
    translation = TranslationField(
        MenuItemTranslation,
        type_name="menu item",
        resolver=ChannelContextType.resolve_translation,
    )

    class Meta:
        default_resolver = ChannelContextType.resolver_with_context

        description = (
            "Represents a single item of the related menu. Can store categories, "
            "collection or pages."
        )
        interfaces = [relay.Node]
        only_fields = [
            "category",
            "collection",
            "id",
            "level",
            "menu",
            "name",
            "page",
            "parent",
        ]
        model = models.MenuItem

    @staticmethod
    def resolve_category(root: ChannelContext[models.MenuItem], info, **_kwargs):
        if root.node.category_id:
            return CategoryByIdLoader(info.context).load(root.node.category_id)
        return None

    @staticmethod
    def resolve_children(root: ChannelContext[models.MenuItem], info, **_kwargs):
        menus = MenuItemChildrenLoader(info.context).load(root.node.id)
        return menus.then(
            lambda menus: [
                ChannelContext(node=menu, channel_slug=root.channel_slug)
                for menu in menus
            ]
        )

    @staticmethod
    def resolve_collection(root: ChannelContext[models.MenuItem], info, **_kwargs):
        if not root.node.collection_id:
            return None

        requestor = get_user_or_app_from_context(info.context)
        requestor_has_access_to_all = Collection.objects.user_has_access_to_all(
            requestor
        )
        if requestor_has_access_to_all:
            return (
                CollectionByIdLoader(info.context)
                .load(root.node.collection_id)
                .then(
                    lambda collection: ChannelContext(
                        node=collection, channel_slug=root.channel_slug
                    )
                    if collection
                    else None
                )
            )

        # If it's a non-staff user with proper permission we should check that
        # channel is active and collection is visible in this channel
        channel_slug = str(root.channel_slug)
        channel = ChannelBySlugLoader(info.context).load(channel_slug)

        def calculate_collection_availability(collection_channel_listing):
            def calculate_collection_availability_with_channel(channel):
                collection_is_visible = (
                    collection_channel_listing.is_visible
                    if collection_channel_listing
                    else False
                )
                if not channel.is_active or not collection_is_visible:
                    return None
                return (
                    CollectionByIdLoader(info.context)
                    .load(root.node.collection_id)
                    .then(
                        lambda collection: ChannelContext(
                            node=collection, channel_slug=channel_slug
                        )
                        if collection
                        else None
                    )
                )

            return channel.then(calculate_collection_availability_with_channel)

        return (
            CollectionChannelListingByCollectionIdAndChannelSlugLoader(info.context)
            .load((root.node.collection_id, channel_slug))
            .then(calculate_collection_availability)
        )

    @staticmethod
    def resolve_menu(root: ChannelContext[models.MenuItem], info, **_kwargs):
        if root.node.menu_id:
            menu = MenuByIdLoader(info.context).load(root.node.menu_id)
            return menu.then(
                lambda menu: ChannelContext(node=menu, channel_slug=root.channel_slug)
            )
        return None

    @staticmethod
    def resolve_parent(root: ChannelContext[models.MenuItem], info, **_kwargs):
        if root.node.parent_id:
            menu = MenuItemByIdLoader(info.context).load(root.node.parent_id)
            return menu.then(
                lambda menu: ChannelContext(node=menu, channel_slug=root.channel_slug)
            )
        return None

    @staticmethod
    def resolve_page(root: ChannelContext[models.MenuItem], info, **kwargs):
        if root.node.page_id:
            requestor = get_user_or_app_from_context(info.context)
            requestor_has_access_to_all = requestor.is_active and requestor.has_perm(
                PagePermissions.MANAGE_PAGES
            )
            return (
                PageByIdLoader(info.context)
                .load(root.node.page_id)
                .then(
                    lambda page: page
                    if requestor_has_access_to_all or page.is_visible
                    else None
                )
            )
        return None


class MenuItemMoveInput(graphene.InputObjectType):
    item_id = graphene.ID(description="The menu item ID to move.", required=True)
    parent_id = graphene.ID(
        description="ID of the parent menu. If empty, menu will be top level menu."
    )
    sort_order = graphene.Int(
        description=(
            "The new relative sorting position of the item (from -inf to +inf). "
            "1 moves the item one position forward, -1 moves the item one position "
            "backward, 0 leaves the item unchanged."
        )
    )
